import { CairoInt32, constants } from '../../../src';

const { PRIME } = constants;

describe('CairoInt32 class Unit Tests', () => {
  describe('constructor with different input types', () => {
    test('should handle positive number input', () => {
      const i32 = new CairoInt32(1000000);
      expect(i32.data).toBe(1000000n);
    });

    test('should handle negative number input', () => {
      const i32 = new CairoInt32(-1000000);
      expect(i32.data).toBe(-1000000n);
    });

    test('should handle bigint input', () => {
      const i32 = new CairoInt32(123456789n);
      expect(i32.data).toBe(123456789n);
    });

    test('should handle negative bigint input', () => {
      const i32 = new CairoInt32(-123456789n);
      expect(i32.data).toBe(-123456789n);
    });

    test('should handle zero values', () => {
      const i32FromNumber = new CairoInt32(0);
      const i32FromBigint = new CairoInt32(0n);

      expect(i32FromNumber.data).toBe(0n);
      expect(i32FromBigint.data).toBe(0n);
    });

    test('should handle maximum i32 value', () => {
      const maxI32 = 2147483647n; // 2^31 - 1
      const i32 = new CairoInt32(maxI32);
      expect(i32.data).toBe(maxI32);
    });

    test('should handle minimum i32 value', () => {
      const minI32 = -2147483648n; // -2^31
      const i32 = new CairoInt32(minI32);
      expect(i32.data).toBe(minI32);
    });
  });

  describe('validation', () => {
    test('should accept valid i32 values', () => {
      expect(() => new CairoInt32(-2147483648)).not.toThrow();
      expect(() => new CairoInt32(0)).not.toThrow();
      expect(() => new CairoInt32(2147483647)).not.toThrow();
      expect(() => new CairoInt32('A')).not.toThrow(); // UTF-8 encoded to 65
      expect(() => new CairoInt32(1000000n)).not.toThrow();
      expect(() => new CairoInt32(-1000000n)).not.toThrow();
    });

    test('should reject values less than -2^31', () => {
      expect(() => new CairoInt32(-2147483649)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => new CairoInt32(-3000000000n)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
    });

    test('should reject values greater than 2^31-1', () => {
      expect(() => new CairoInt32(2147483648)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => new CairoInt32(3000000000n)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => new CairoInt32('3000000000')).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
    });

    test('should handle valid string inputs correctly', () => {
      const i32FromCharString = new CairoInt32('A'); // UTF-8 encoded to 65
      const i32FromNumString = new CairoInt32('1000'); // Parsed as number 1000
      const i32FromHexString = new CairoInt32('0x7fffffff');

      expect(i32FromCharString.data).toBe(65n); // ASCII value of 'A'
      expect(i32FromNumString.data).toBe(1000n); // Parsed as number
      expect(i32FromHexString.data).toBe(2147483647n);
    });

    test('should reject decimal numbers', () => {
      expect(() => new CairoInt32(42.5)).toThrow(
        'Invalid input: decimal numbers are not supported, only integers'
      );
      expect(() => new CairoInt32(-1.1)).toThrow(
        'Invalid input: decimal numbers are not supported, only integers'
      );
    });

    test('should handle edge cases and invalid inputs', () => {
      expect(() => CairoInt32.validate(null as any)).toThrow('Invalid input: null or undefined');
      expect(() => CairoInt32.validate(undefined as any)).toThrow(
        'Invalid input: null or undefined'
      );
      expect(() => CairoInt32.validate({} as any)).toThrow(
        'Invalid input: objects are not supported'
      );
      expect(() => CairoInt32.validate([] as any)).toThrow(
        'Invalid input: objects are not supported'
      );
    });
  });

  describe('toBigInt method', () => {
    test('should return the stored bigint value', () => {
      const values = [-2147483648, -1000000, 0, 1000000, 2147483647];
      values.forEach((val) => {
        const i32 = new CairoInt32(val);
        expect(i32.toBigInt()).toBe(BigInt(val));
      });
    });

    test('should handle negative values', () => {
      const i32 = new CairoInt32(-1234567);
      expect(i32.toBigInt()).toBe(-1234567n);
    });

    test('should handle boundary values', () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);
      expect(minI32.toBigInt()).toBe(-2147483648n);
      expect(maxI32.toBigInt()).toBe(2147483647n);
    });
  });

  describe('toHexString method', () => {
    test('should convert zero to hex', () => {
      const i32 = new CairoInt32(0);
      expect(i32.toHexString()).toBe('0x0');
    });

    test('should convert positive numbers to hex', () => {
      const i32 = new CairoInt32(65535);
      expect(i32.toHexString()).toBe('0xffff');
    });

    test('should convert negative numbers to hex using field element representation', () => {
      const i32 = new CairoInt32(-1);
      // -1 becomes PRIME + (-1) = PRIME - 1
      const fieldElement = PRIME - 1n;
      expect(i32.toHexString()).toBe(`0x${fieldElement.toString(16)}`);
    });

    test('should convert boundary values to hex', () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);
      const minFieldElement = PRIME - 2147483648n;
      expect(minI32.toHexString()).toBe(`0x${minFieldElement.toString(16)}`);
      expect(maxI32.toHexString()).toBe('0x7fffffff');
    });
  });

  describe('decodeUtf8 method', () => {
    test('should decode positive values correctly', () => {
      const i32 = new CairoInt32(65); // 'A'
      expect(i32.decodeUtf8()).toBe('A');
    });

    test("should handle negative values with two's complement", () => {
      const i32 = new CairoInt32(-1);
      // Negative values are converted using 2^32 + value for UTF-8 decoding
      expect(typeof i32.decodeUtf8()).toBe('string');
    });

    test('should handle boundary values', () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);
      expect(typeof minI32.decodeUtf8()).toBe('string');
      expect(typeof maxI32.decodeUtf8()).toBe('string');
    });
  });

  describe('validate static method', () => {
    test('should validate correct number inputs', () => {
      expect(() => CairoInt32.validate(-2147483648)).not.toThrow();
      expect(() => CairoInt32.validate(0)).not.toThrow();
      expect(() => CairoInt32.validate(2147483647)).not.toThrow();
    });

    test('should validate correct bigint inputs', () => {
      expect(() => CairoInt32.validate(-2147483648n)).not.toThrow();
      expect(() => CairoInt32.validate(0n)).not.toThrow();
      expect(() => CairoInt32.validate(2147483647n)).not.toThrow();
    });

    test('should reject out-of-range values', () => {
      expect(() => CairoInt32.validate(-2147483649)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => CairoInt32.validate(2147483648)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => CairoInt32.validate(-3000000000n)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
      expect(() => CairoInt32.validate(3000000000n)).toThrow(
        'Value is out of i32 range [-2147483648, 2147483647]'
      );
    });

    test('should reject invalid types', () => {
      expect(() => CairoInt32.validate(null as any)).toThrow('Invalid input: null or undefined');
      expect(() => CairoInt32.validate(undefined as any)).toThrow(
        'Invalid input: null or undefined'
      );
      expect(() => CairoInt32.validate({} as any)).toThrow(
        'Invalid input: objects are not supported'
      );
      expect(() => CairoInt32.validate([] as any)).toThrow(
        'Invalid input: objects are not supported'
      );
    });
  });

  describe('is static method', () => {
    test('should return true for valid inputs', () => {
      expect(CairoInt32.is(-2147483648)).toBe(true);
      expect(CairoInt32.is(0)).toBe(true);
      expect(CairoInt32.is(2147483647)).toBe(true);
      expect(CairoInt32.is(-1000000n)).toBe(true);
      expect(CairoInt32.is('A')).toBe(true); // UTF-8 encoded to 65
      expect(CairoInt32.is('1000')).toBe(true); // Parsed as number 1000
    });

    test('should return false for invalid inputs', () => {
      expect(CairoInt32.is(-2147483649)).toBe(false);
      expect(CairoInt32.is(2147483648)).toBe(false);
      expect(CairoInt32.is(null as any)).toBe(false);
      expect(CairoInt32.is(undefined as any)).toBe(false);
      expect(CairoInt32.is({} as any)).toBe(false);
      expect(CairoInt32.is(42.5)).toBe(false);
    });
  });

  describe('isAbiType static method', () => {
    test('should identify correct ABI type', () => {
      expect(CairoInt32.isAbiType('core::integer::i32')).toBe(true);
      expect(CairoInt32.isAbiType('core::integer::i16')).toBe(false);
      expect(CairoInt32.isAbiType('core::integer::u32')).toBe(false);
      expect(CairoInt32.isAbiType('felt252')).toBe(false);
    });
  });

  describe('signed integer specific tests', () => {
    test('should handle negative values correctly', () => {
      const negativeValues = [-2147483648, -1000000, -1000, -1];
      negativeValues.forEach((val) => {
        const i32 = new CairoInt32(val);
        expect(i32.data).toBe(BigInt(val));
        expect(i32.toBigInt()).toBe(BigInt(val));
      });
    });

    test("should handle two's complement boundary correctly", () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);

      expect(minI32.data).toBe(-2147483648n);
      expect(maxI32.data).toBe(2147483647n);

      // Test that values outside range are rejected
      expect(() => new CairoInt32(-2147483649)).toThrow();
      expect(() => new CairoInt32(2147483648)).toThrow();
    });

    test('should maintain sign consistency', () => {
      const testCases = [
        { input: -1000000, expected: -1000000n },
        { input: 1000000, expected: 1000000n },
        { input: 'A', expected: 65n }, // UTF-8 encoded
        { input: '5', expected: 5n }, // Parsed as number
        { input: -1n, expected: -1n },
        { input: 1n, expected: 1n },
      ];

      testCases.forEach(({ input, expected }) => {
        const i32 = new CairoInt32(input as any);
        expect(i32.data).toBe(expected);
        expect(i32.toBigInt()).toBe(expected);
      });
    });
  });

  describe('toApiRequest method', () => {
    test('should return hex string array for zero', () => {
      const i32 = new CairoInt32(0);
      const result = i32.toApiRequest();
      expect(result).toEqual(['0x0']);
      expect(result).toHaveProperty('__compiled__', true);
    });

    test('should return hex string array for positive numbers', () => {
      const i32 = new CairoInt32(1000000);
      const result = i32.toApiRequest();
      expect(result).toEqual(['0xf4240']);
      expect(result).toHaveProperty('__compiled__', true);
    });

    test('should return field element hex representation for negative numbers', () => {
      const i32 = new CairoInt32(-1000000);
      const result = i32.toApiRequest();
      // Negative value -1000000 becomes PRIME + (-1000000) = PRIME - 1000000
      const fieldElement = PRIME - 1000000n;
      const expectedValue = `0x${fieldElement.toString(16)}`;
      expect(result).toEqual([expectedValue]);
      expect(result).toHaveProperty('__compiled__', true);
    });

    test('should handle boundary values', () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);
      const minFieldElement = PRIME - 2147483648n;
      const expectedMinValue = `0x${minFieldElement.toString(16)}`;
      const expectedMaxValue = '0x7fffffff';
      expect(minI32.toApiRequest()).toEqual([expectedMinValue]);
      expect(maxI32.toApiRequest()).toEqual([expectedMaxValue]);
    });
  });

  describe('factoryFromApiResponse method', () => {
    test('should create CairoInt32 from API response iterator', () => {
      const mockIterator = {
        next: jest.fn().mockReturnValue({ value: '0xf4240', done: false }),
      };
      const i32 = CairoInt32.factoryFromApiResponse(mockIterator as any);
      expect(i32.data).toBe(1000000n);
    });

    test('should handle positive values from API response', () => {
      const mockIterator = {
        next: jest.fn().mockReturnValue({ value: '1000000', done: false }),
      };
      const i32 = CairoInt32.factoryFromApiResponse(mockIterator as any);
      expect(i32.data).toBe(1000000n);
    });

    test('should handle boundary values from API response', () => {
      const mockIterator = {
        next: jest.fn().mockReturnValue({ value: '2147483647', done: false }),
      };
      const i32 = CairoInt32.factoryFromApiResponse(mockIterator as any);
      expect(i32.data).toBe(2147483647n);
    });
  });

  describe('round-trip consistency', () => {
    test('should maintain consistency between constructor types', () => {
      const testValues = [-2147483648, -1000000, 0, 1000000, 2147483647];
      testValues.forEach((val) => {
        const i32FromNumber = new CairoInt32(val);
        const i32FromBigint = new CairoInt32(BigInt(val));
        // Skip string comparison as strings are UTF-8 encoded and produce different values

        expect(i32FromNumber.toBigInt()).toBe(i32FromBigint.toBigInt());
      });
    });

    test('should handle round-trip conversions', () => {
      const originalValue = -1234567;
      const i32 = new CairoInt32(originalValue);
      const bigintValue = i32.toBigInt();
      const newI32 = new CairoInt32(bigintValue);

      expect(newI32.toBigInt()).toBe(BigInt(originalValue));
      expect(newI32.data).toBe(i32.data);
    });
  });

  describe('edge cases and consistency checks', () => {
    test('should handle boundary values correctly', () => {
      const minI32 = new CairoInt32(-2147483648);
      const maxI32 = new CairoInt32(2147483647);

      expect(minI32.data).toBe(-2147483648n);
      expect(maxI32.data).toBe(2147483647n);
      expect(minI32.toBigInt()).toBe(-2147483648n);
      expect(maxI32.toBigInt()).toBe(2147483647n);
    });

    test('should maintain consistency across methods', () => {
      const values = [-2147483648, -1000000, 0, 1000000, 2147483647];
      values.forEach((val) => {
        const i32 = new CairoInt32(val);
        const bigintVal = i32.toBigInt();
        const hexVal = i32.toHexString();
        const apiRequest = i32.toApiRequest();

        expect(bigintVal).toBe(BigInt(val));
        // For negative values, hex uses field element representation
        if (val < 0) {
          const fieldElement = PRIME + BigInt(val);
          expect(hexVal).toBe(`0x${fieldElement.toString(16)}`);
        } else {
          expect(hexVal).toBe(`0x${val.toString(16)}`);
        }
        // apiRequest should equal hexVal
        expect(apiRequest[0]).toBe(hexVal);
      });
    });

    test('should preserve exact values without precision loss', () => {
      const testValues = [-2147483648, -1234567, 0, 1234567, 2147483647];
      testValues.forEach((val) => {
        const i32 = new CairoInt32(val);
        expect(i32.toBigInt()).toBe(BigInt(val));
        expect(Number(i32.toBigInt())).toBe(val);
      });
    });
  });

  describe('JavaScript integer compatibility', () => {
    test('should handle all JavaScript safe integers', () => {
      const safeIntegerValues = [
        Number.MIN_SAFE_INTEGER,
        -1000000,
        -1,
        0,
        1,
        1000000,
        Number.MAX_SAFE_INTEGER,
      ].filter((val) => val >= -2147483648 && val <= 2147483647);

      safeIntegerValues.forEach((val) => {
        const i32 = new CairoInt32(val);
        expect(i32.toBigInt()).toBe(BigInt(val));
        expect(Number(i32.toBigInt())).toBe(val);
      });
    });

    test('should handle powers of 2 within range', () => {
      const powersOf2 = [
        1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072,
        262144, 524288, 1048576,
      ];
      powersOf2.forEach((power) => {
        const i32Pos = new CairoInt32(power);
        const i32Neg = new CairoInt32(-power);
        expect(i32Pos.toBigInt()).toBe(BigInt(power));
        expect(i32Neg.toBigInt()).toBe(BigInt(-power));
      });
    });
  });
});
